import scrapy
from urllib.parse import urljoin, urlparse
import re
import time
import hashlib
import json
from pathlib import Path
import os

MAX_PAGES = 100100 

class NankaiSpider(scrapy.Spider):
    name = "nankai"
    allowed_domains = ["nankai.edu.cn"]
    start_urls = [
        "https://www.nankai.edu.cn/",       # 主站
        "https://news.nankai.edu.cn/",      # 新闻网
        "https://cc.nankai.edu.cn/",        # 计算机学院
        "https://jwc.nankai.edu.cn/",       # 教务处
        "https://lib.nankai.edu.cn/",       # 图书馆
        "https://cs.nankai.edu.cn/",        # 软件学院
        "https://math.nankai.edu.cn/",      # 数学科学学院
        "https://sky.nankai.edu.cn/",       # 生命科学学院
        "https://physics.nankai.edu.cn/",   # 物理科学学院
        "https://chem.nankai.edu.cn/",      # 化学学院
        "https://env.nankai.edu.cn/",       # 环境科学与工程学院
        "https://economics.nankai.edu.cn/", # 经济学院
        "https://history.nankai.edu.cn/",   # 历史学院
        "https://phil.nankai.edu.cn/",      # 哲学院
        "https://wxy.nankai.edu.cn/",       # 文学院
        "https://law.nankai.edu.cn/",       # 法学院
        "https://zfxy.nankai.edu.cn/",      # 周恩来政府管理学院
        "https://sfs.nankai.edu.cn/",       # 外国语学院
        "https://mse.nankai.edu.cn/",       # 材料科学与工程学院
        "https://pharmacy.nankai.edu.cn/",  # 药学院
        "https://nkuef.nankai.edu.cn/",     # 南开大学教育基金会
        "https://zsb.nankai.edu.cn/",       # 招生办公室
        "https://stat.nankai.edu.cn/",      # 统计学院
        "https://binhai.nankai.edu.cn/",    # 滨海学院
        "https://xxgk.nankai.edu.cn/",      # 信息公开
        "https://sjc.nankai.edu.cn/",       # 审计处
        "https://ceo.nankai.edu.cn/",       # 电子信息与光学工程学院
        "https://jc.nankai.edu.cn/",        # 新闻与传播学院
        "https://art.nankai.edu.cn/",       # 艺术与美学研究院
        "https://graduate.nankai.edu.cn/",  # 研究生院
        "https://online.nankai.edu.cn/",    # 网上办事大厅
        "https://yzb.nankai.edu.cn/",       # 研究生招生网
        "https://international.nankai.edu.cn/", # 国际合作与交流处
        "https://career.nankai.edu.cn/",      # 就业指导中心
        "https://xb.nankai.edu.cn/",        # 办公网
        "https://bs.nankai.edu.cn/",        # 商学院
        "https://cwc.nankai.edu.cn/",       # 财务处
        "https://tas.nankai.edu.cn/",       # 旅游与服务学院
        "https://cz.nankai.edu.cn/",        # 马克思主义学院
        "https://sklpmc.nankai.edu.cn/",   
        "https://nkiie.nankai.edu.cn/",     # 国际经济研究所
        "https://medical.nankai.edu.cn/",    # 医学院
        "https://mbaxw.nankai.edu.cn/",
        "https://cias.nankai.edu.cn/",
        "https://panglab.nankai.edu.cn/",
        "https://dulab.nankai.edu.cn/",
        "https://ioip.nankai.edu.cn/",
        "https://esd.nankai.edu.cn/",
        "https://tedabio.nankai.edu.cn/",
        "https://wxb.nankai.edu.cn/",
        "https://topo-photonics.nankai.edu.cn/",
        "https://cingai.nankai.edu.cn/",
        "https://gzb.nankai.edu.cn/",
        "https://bulab.nankai.edu.cn/",
        "https://finance.nankai.edu.cn/",
        "https://zeolite.nankai.edu.cn/",
        "https://hyxy.nankai.edu.cn/",
        "https://shxy.nankai.edu.cn/",
        "https://cyber.nankai.edu.cn/",
        "https://tyb.nankai.edu.cn/",
        "https://pt.nankai.edu.cn/",
        "https://teda.nankai.edu.cn/",
        "https://nkszri.nankai.edu.cn/",
        "https://cfc.nankai.edu.cn/",
        "https://nkup.nankai.edu.cn/",
        "https://lebps.nankai.edu.cn/",
        "https://nkjd.nankai.edu.cn/",
        "https://robot.nankai.edu.cn/",
        "https://konglab.nankai.edu.cn/",
        "https://nanocenter.nankai.edu.cn/",
        "https://fy.nankai.edu.cn/",
        "https://std.nankai.edu.cn/",
        "https://lac.nankai.edu.cn/",
        "https://ics.nankai.edu.cn/",
        "https://nbjl.nankai.edu.cn/",
        "https://cui.nankai.edu.cn/",
        "https://imo.nankai.edu.cn/",
        "https://swsyzx.nankai.edu.cn/",
        "https://econlab.nankai.edu.cn/",
        "https://nbr.nankai.edu.cn/",
        "https://zzzx.nankai.edu.cn/",
        "https://guard.nankai.edu.cn/",
        "https://skleoc.nankai.edu.cn/",
        "https://icell.nankai.edu.cn/",
        "https://sie.nankai.edu.cn/",
        "https://rsc.nankai.edu.cn/",
        "https://logic.nankai.edu.cn/",
        "https://aemc.nankai.edu.cn/",
        "https://iap.nankai.edu.cn/",
        "https://tms.nankai.edu.cn/",
        "https://hq.nankai.edu.cn/",
        "https://ilab.nankai.edu.cn/",
        "https://gh.nankai.edu.cn/",
    ] 
    
    # 定义文档扩展名，用于识别文档链接
    document_extensions = re.compile(r'\.(pdf|doc|docx|xls|xlsx|ppt|pptx|zip|rar)$', re.IGNORECASE)
    
    # 媒体扩展名，仍然过滤这些内容
    media_extensions = re.compile(r'\.(jpg|jpeg|png|gif|mp4|mp3|mov|avi)$', re.IGNORECASE)

    # 爬虫设置
    custom_settings = {
        'ROBOTSTXT_OBEY': True,
        'DOWNLOAD_DELAY': 0.2,
        'CONCURRENT_REQUESTS': 16,
        'USER_AGENT': 'Mozilla/5.0 (compatible; NankaiSpider/1.0; +https://www.nankai.edu.cn/)',
        'DEPTH_LIMIT': 12,
        'DOWNLOAD_TIMEOUT': 15,
        'RETRY_TIMES': 3,
        'LOG_LEVEL': 'INFO',
        'AUTOTHROTTLE_ENABLED': True,
        'AUTOTHROTTLE_START_DELAY': 1.0,
        'AUTOTHROTTLE_TARGET_CONCURRENCY': 8.0,
        'JOBDIR': 'crawls/nankai-spider',
        'FILES_STORE': 'data/documents',
    }
    
    def __init__(self, *args, **kwargs):
        super(NankaiSpider, self).__init__(*args, **kwargs)
        self.pages_crawled = 0  # 记录已爬取的页面数量
        self.crawled_urls = set()  # 用于记录已爬取的URL
        self.document_urls = set()  # 用于记录文档URL
        self.revisited_urls = set()  # 用于记录已重访但不重复爬取的URL
        
        # 链接发现模式，默认为False
        self.link_discovery_mode = kwargs.get('link_discovery', False)
        if self.link_discovery_mode:
            self.logger.info("启用链接发现模式：将重新访问已爬取URL以发现新链接，但不会重复爬取")
            
            # 在链接发现模式下修改设置，禁用URL去重过滤器
            self.custom_settings = {
                'ROBOTSTXT_OBEY': True,
                'DOWNLOAD_DELAY': 0.2,
                'CONCURRENT_REQUESTS': 16,
                'USER_AGENT': 'Mozilla/5.0 (compatible; NankaiSpider/1.0; +https://www.nankai.edu.cn/)',
                'DEPTH_LIMIT': 12,
                'DOWNLOAD_TIMEOUT': 15,
                'RETRY_TIMES': 3,
                'LOG_LEVEL': 'INFO',
                'AUTOTHROTTLE_ENABLED': True,
                'AUTOTHROTTLE_START_DELAY': 1.0,
                'AUTOTHROTTLE_TARGET_CONCURRENCY': 8.0,
                'JOBDIR': 'crawls/nankai-spider-discovery',
                'FILES_STORE': 'data/documents',
                # 关键设置：禁用URL去重过滤器
                'DUPEFILTER_CLASS': 'scrapy.dupefilters.BaseDupeFilter',
            }
            self.logger.info("链接发现模式：已禁用URL去重过滤器")
        
        # 确保文档存储目录存在
        os.makedirs('data/documents', exist_ok=True)
        
        # 加载断点续传状态
        self.state_file = Path("data/crawler_state.json")
        if self.state_file.exists():
            try:
                with open(self.state_file, "r", encoding="utf-8") as f:
                    state = json.load(f)
                    self.pages_crawled = state.get("pages_crawled", 0)
                    self.logger.info(f"从断点恢复，已爬取 {self.pages_crawled} 页")
                    
                    # 加载已爬取的URL
                    crawled_urls_file = Path("data/crawled_urls.txt")
                    if crawled_urls_file.exists():
                        with open(crawled_urls_file, "r", encoding="utf-8") as f_urls:
                            self.crawled_urls = set(line.strip() for line in f_urls)
                        self.logger.info(f"加载已爬取URL: {len(self.crawled_urls)} 个")
                    
                    # 加载文档URL
                    document_urls_file = Path("data/document_urls.txt")
                    if document_urls_file.exists():
                        with open(document_urls_file, "r", encoding="utf-8") as f_docs:
                            self.document_urls = set(line.strip() for line in f_docs)
                        self.logger.info(f"加载文档URL: {len(self.document_urls)} 个")
                    
                    # 加载已重访URL
                    revisited_urls_file = Path("data/revisited_urls.txt")
                    if revisited_urls_file.exists():
                        with open(revisited_urls_file, "r", encoding="utf-8") as f_revisited:
                            self.revisited_urls = set(line.strip() for line in f_revisited)
                        self.logger.info(f"加载已重访URL: {len(self.revisited_urls)} 个")
            except Exception as e:
                self.logger.error(f"加载断点状态失败: {e}")
    
    def start_requests(self):
        """重写起始请求生成方法，处理链接发现模式"""
        if self.link_discovery_mode:
            # 链接发现模式：从已爬取的URL中选择一些重新访问
            self.logger.info(f"链接发现模式：将重新访问已爬取的URL")
            
            # 确定要重新访问的URL
            to_revisit = []
            count = 0
            limit = 5000  # 每次限制访问的数量
            
            for url in self.crawled_urls:
                # 跳过已重访过的URL、文档URL和媒体URL
                if (url in self.revisited_urls or 
                    self.document_extensions.search(url) or
                    self.media_extensions.search(url)):
                    continue
                
                # 有效的URL加入重访列表
                parsed = urlparse(url)
                if parsed.netloc.endswith('nankai.edu.cn') and parsed.scheme in ['http', 'https']:
                    to_revisit.append(url)
                    count += 1
                    
                    if count >= limit:
                        break
            
            self.logger.info(f"链接发现模式：将重新访问 {count} 个URL")
            
            # 生成重访请求
            for url in to_revisit:
                # 添加随机参数，欺骗URL去重过滤器
                timestamp = int(time.time() * 1000)
                modified_url = url
                if '?' in url:
                    modified_url = f"{url}&_ts={timestamp}"
                else:
                    modified_url = f"{url}?_ts={timestamp}"
                    
                yield scrapy.Request(
                    modified_url,
                    callback=self.parse_link_discovery,
                    meta={'dont_redirect': True, 'original_url': url},
                    errback=self.handle_error
                )
        else:
            # 普通模式：直接从start_urls开始
            for url in self.start_urls:
                yield scrapy.Request(
                    url,
                    callback=self.parse,
                    errback=self.handle_error
                )
    
    def parse_link_discovery(self, response):
        """链接发现模式专用的解析函数，只提取链接但不保存页面"""
        current_url = response.meta.get('original_url', response.url)
        
        # 标记此URL已重访
        self.revisited_urls.add(current_url)
        
        # 每处理100个URL保存一次状态
        if len(self.revisited_urls) % 100 == 0:
            self.logger.info(f"链接发现模式：已重访 {len(self.revisited_urls)} 个URL")
            self.save_state()
        
        try:
            # 仅处理链接，不保存页面
            new_links_found = 0
            
            # 处理页面中的所有链接
            for href in response.css('a::attr(href)').getall():
                # 跳过空链接或锚点链接
                if not href or href.startswith('#'):
                    continue
                
                # 过滤媒体文件
                if self.media_extensions.search(href):
                    continue
                
                url = urljoin(current_url, href)
                parsed = urlparse(url)
                
                # 确保是南开域名且为HTTP(S)协议
                if (parsed.netloc.endswith('nankai.edu.cn') and 
                    parsed.scheme in ['http', 'https']):
                    
                    # 如果是新链接且未爬取过
                    if url not in self.crawled_urls:
                        new_links_found += 1
                        
                        # 检查是否是文档链接
                        if self.document_extensions.search(url):
                            if url not in self.document_urls:
                                self.document_urls.add(url)
                                yield scrapy.Request(
                                    url, 
                                    callback=self.handle_document,
                                    errback=self.handle_error,
                                    meta={'dont_redirect': True}
                                )
                        else:
                            # 普通网页，添加到爬取队列
                            yield scrapy.Request(
                                url, 
                                callback=self.parse,  # 使用普通解析函数
                                errback=self.handle_error,
                                meta={'dont_redirect': True}
                            )
            
            if new_links_found > 0:
                self.logger.info(f"在 {current_url} 中发现 {new_links_found} 个新链接")
                
        except Exception as e:
            self.logger.error(f"解析页面 {current_url} 时出错: {e}")
    
    def parse(self, response):
        current_url = response.url
        
        # 检查此URL是否已处理过
        if current_url in self.crawled_urls:
            return
            
        # 标记为已爬取
        self.crawled_urls.add(current_url)
        
        if self.pages_crawled >= MAX_PAGES:
            self.logger.info(f"已达到目标页面数 {MAX_PAGES}，停止爬取")
            return
            
        try:
            # 检查是否是文档文件
            if self.document_extensions.search(current_url):
                # 如果是文档，使用不同的处理逻辑
                self.handle_document(response)
                return
            
            # 计算网页内容的哈希值，用于去重和快照索引
            page_hash = hashlib.md5(response.body).hexdigest()
            
            # 提取页面信息，构建更完整的数据结构
            data = {
                'url': current_url,
                'domain': urlparse(current_url).netloc,
                'title': response.xpath('//title/text()').get() or '',
                'text': ' '.join(response.xpath('//body//text()').getall()).strip(),
                'meta_keywords': response.xpath('//meta[@name="keywords"]/@content').get() or '',
                'meta_description': response.xpath('//meta[@name="description"]/@content').get() or '',
                'html_snapshot': response.text[:10000],  # 减少HTML快照大小
                'crawl_time': time.strftime('%Y-%m-%d %H:%M:%S'),
                'page_hash': page_hash,
                # 提取页面链接，用于后续分析
                'links': list(set([urljoin(current_url, href) for href in response.css('a::attr(href)').getall() if href])),
                # 提取重要文本段落，便于摘要生成
                'paragraphs': response.xpath('//p/text()').getall(),
                # 提取图片URL，用于搜索结果展示
                'images': response.xpath('//img/@src').getall()
            }
            
            # 记录和输出进度信息
            self.pages_crawled += 1
            if self.pages_crawled % 100 == 0:
                self.logger.info(f"已爬取 {self.pages_crawled} 页面")
                # 每100页保存一次状态
                self.save_state()

            yield data
            
            # 从页面中提取文档链接
            yield from self.extract_document_links(response)
                
            # 递归抓取校内链接
            if self.pages_crawled < MAX_PAGES:
                # 处理页面中的所有链接
                for href in response.css('a::attr(href)').getall():
                    # 跳过空链接或锚点链接
                    if not href or href.startswith('#'):
                        continue
                    
                    # 过滤媒体文件，但允许普通网页
                    if self.media_extensions.search(href):
                        continue
                    
                    url = urljoin(current_url, href)
                    parsed = urlparse(url)
                    
                    # 确保是南开域名且为HTTP(S)协议，且未爬取过
                    if (parsed.netloc.endswith('nankai.edu.cn') and 
                        parsed.scheme in ['http', 'https'] and
                        url not in self.crawled_urls):
                        
                        # 检查是否是文档链接 - 已在extract_document_links处理，这里只需处理普通网页
                        if not self.document_extensions.search(url):
                            yield scrapy.Request(
                                url, 
                                callback=self.parse,
                                meta={'dont_redirect': True},
                                errback=self.handle_error
                            )
        except Exception as e:
            self.logger.error(f"解析页面 {current_url} 时出错: {e}")
    
    def extract_document_links(self, response):
        """从页面中提取文档链接并安排爬取"""
        documents_found = 0
        
        for href in response.css('a::attr(href)').getall():
            if not href or href.startswith('#'):
                continue
                
            url = urljoin(response.url, href)
            
            # 检查是否是文档链接
            if self.document_extensions.search(url):
                parsed = urlparse(url)
                
                # 确保是南开域名且为HTTP(S)协议
                if (parsed.netloc.endswith('nankai.edu.cn') and 
                    parsed.scheme in ['http', 'https'] and
                    url not in self.document_urls):
                    
                    self.document_urls.add(url)
                    documents_found += 1
                    
                    yield scrapy.Request(
                        url, 
                        callback=self.handle_document,
                        errback=self.handle_error,
                        meta={'dont_redirect': True}
                    )
        
        if documents_found > 0:
            self.logger.info(f"在 {response.url} 中发现 {documents_found} 个新文档链接")
    
    def handle_document(self, response):
        """处理文档文件"""
        try:
            # 获取文件名
            filename = response.url.split('/')[-1]
            file_path = f'data/documents/{filename}'
            
            # 保存文件
            with open(file_path, 'wb') as f:
                f.write(response.body)
            
            # 记录文档信息
            data = {
                'url': response.url,
                'domain': urlparse(response.url).netloc,
                'title': filename,  # 使用文件名作为标题
                'file_type': filename.split('.')[-1].lower(),
                'file_size': len(response.body),
                'crawl_time': time.strftime('%Y-%m-%d %H:%M:%S'),
                'file_path': file_path,
                'content_type': response.headers.get('Content-Type', b'').decode('utf-8', errors='ignore')
            }
            
            # 更新计数
            self.pages_crawled += 1
            if self.pages_crawled % 100 == 0:
                self.logger.info(f"已爬取 {self.pages_crawled} 页面 (包括 {len(self.document_urls)} 个文档)")
                self.save_state()
            
            yield data
            
        except Exception as e:
            self.logger.error(f"处理文档 {response.url} 时出错: {e}")
    
    def handle_error(self, failure):
        # 简单记录错误，但不中断爬虫
        self.logger.warning(f"请求失败: {failure.request.url}, 原因: {failure.value}")
        
    def save_state(self):
        """保存爬虫状态，用于断点续传"""
        Path("data").mkdir(exist_ok=True)
        try:
            # 保存爬取数量
            with open(self.state_file, "w", encoding="utf-8") as f:
                json.dump({"pages_crawled": self.pages_crawled}, f)
                
            # 保存已爬取URL列表
            with open("data/crawled_urls.txt", "w", encoding="utf-8") as f:
                for url in self.crawled_urls:
                    f.write(f"{url}\n")
                    
            # 保存文档URL列表
            with open("data/document_urls.txt", "w", encoding="utf-8") as f:
                for url in self.document_urls:
                    f.write(f"{url}\n")
            
            # 保存已重访URL列表
            with open("data/revisited_urls.txt", "w", encoding="utf-8") as f:
                for url in self.revisited_urls:
                    f.write(f"{url}\n")
        except Exception as e:
            self.logger.error(f"保存状态失败: {e}")
            
    def closed(self, reason):
        """爬虫关闭时调用，确保保存状态"""
        self.save_state()
        
        # 计算和输出统计信息
        if self.link_discovery_mode:
            self.logger.info(f"链接发现模式下已重访页面: {len(self.revisited_urls)} 个")
        
        self.logger.info(f"爬虫关闭，原因: {reason}，已爬取页面数: {self.pages_crawled} (包括 {len(self.document_urls)} 个文档)") 